import re
import uuid

from redis.client import Redis


def cpc_to_ecpm(views, clicks, cpc):
    return 1000 * cpc * clicks / views


def cpa_to_ecpm(views, actions, cpa):
    return 1000 * cpa * actions / views


TO_ECPM = {
    'cpc': cpc_to_ecpm,
    'cpa': cpa_to_ecpm,
    'cpm': lambda *args: args[-1]
}

AVERAGE_PER_1K = {
    'cpc': 2,
    'cpa': 0.2,
    'cpm': 1000
}

mconn = Redis(host="192.168.35.110", port=6379, db=0, decode_responses=True)

STOP_WORDS = set('able about the a as at to'.split())
WORD_RE = re.compile("[a-z']{2,}")


def tokenize(content):
    words = set()
    for match in WORD_RE.finditer(content.lower()):
        word = match.group().strip("’")
        if len(word) >= 2:
            words.add(word)
    return words - STOP_WORDS


def index_ad(conn, id, locations, content, type, value):
    pipe = conn.pipeline(True)
    for location in locations:
        pipe.sadd('idx:req:' + location, id)
    words = tokenize(content)
    for word in words:
        pipe.zadd('idx:' + word, {id: 0})
    rvalue = TO_ECPM[type](1000, AVERAGE_PER_1K.get(type), value)
    pipe.hset('type', id, type)
    pipe.zadd('idx:ad:value', {id: rvalue})
    pipe.zadd('ad:base_value', {id: value})
    pipe.sadd('terms:' + id, *list(words))
    pipe.execute()


def _set_common(conn, method, names, ttl=30, execute=True):
    id = str(uuid.uuid4())
    pipeline = conn.pipeline(True) if execute else conn
    names = ['idx:' + name for name in names]
    getattr(pipeline, method)('idx:' + id, *names)
    pipeline.expire('idx:' + id, ttl)
    if execute:
        pipeline.execute()
    return id


def intersect(conn, items, ttl=30, _execute=True):
    return _set_common(conn, 'sinterstore', items, ttl, _execute)


def union(conn, items, ttl=30, _execute=True):
    return _set_common(conn, 'sunionstore', items, ttl, _execute)


def difference(conn, items, ttl=30, _execute=True):
    return _set_common(conn, 'sdiffstore', items, ttl, _execute)


def _zset_common(conn, method, scores, ttl=30, **kw):
    id = str(uuid.uuid4())
    execute = kw.pop('_execute', True)
    pipe = conn.pipeline(True) if execute else conn
    for key in list(scores.keys()):
        scores['idx:' + key] = scores.pop(key)
    getattr(pipe, method)('idx:' + id, scores, **kw)
    pipe.expire('idx：' + id, ttl)
    if execute:
        pipe.execute()
    return id


def zintersect(conn, items, ttl=30, **kw):
    return _zset_common(conn, 'zinterstore', dict(items), ttl, **kw)


def zunion(conn, items, ttl=30, **kw):
    return _zset_common(conn, 'zunionstore', dict(items), ttl, **kw)


def target_ads(conn, locations, content):
    pipeline = conn.pipeline(True)
    matched_ads, base_ecpm = match_locaton(pipeline, locations)
    words, target_ads = finish_scoring(pipeline, matched_ads, base_ecpm, content)
    pipeline.incr('ads:served:')
    pipeline.zrevrange('idx:' + target_ads, 0, 0)
    target_id, target_ad = pipeline.execute()[-2:]
    if not target_ad:
        return None, None
    ad_id = target_ad[0]
    return target_id, ad_id


def match_locaton(pipe, locatons):
    required = ['req:' + loc for loc in locatons]
    matched_ads = union(pipe, required, ttl=30, _execute=False)
    return matched_ads, zintersect(pipe, {matched_ads: 0, 'ad:value': 1}, ttl=30, _execute=False)


def finish_scoring(pipe, matched, base, content):
    bonus_ecpm = {}
    words = tokenize(content)
    for word in words:
        word_bonus = zintersect(pipe, {matched: 0, word: 1}, _execute=False)
        bonus_ecpm[word_bonus] = 1
    if bonus_ecpm:
        mininum = zunion(pipe, bonus_ecpm, aggregate='MIN', _execute=False)
        maxinum = zunion(pipe, bonus_ecpm, aggregate='MAX', _execute=False)
        return words, zunion(pipe, {base: 1, mininum: 0.5, maxinum: 0.5}, _execute=False)
    return words, base


def record_targeting_result(conn, target_id, ad_id, words):
    pipe = conn.pipeline(True)
    terms = conn.smembers('terms:' + ad_id)
    matched = list(words & terms)
    if matched:
        matched_key = 'terms:matched:%s' % target_id
        pipe.sadd(matched_key, *matched)
        pipe.expire(matched_key, 900)

    type = conn.hget('type', ad_id)
    pipe.incr('type:%s:views:' % type)
    for word in matched:
        pipe.zincrby('views:%s' % ad_id, float(1.0), word)
    pipe.zincrby('views:%s' % ad_id, float(1.0), '')
    result = pipe.execute()
    print(result)
    if not result[-1] % 100:
        update_cpms(conn, ad_id)


def record_click(conn, target_id, ad_id, action=False):
    pipe = conn.pipeline(True)
    click_key = 'clicks:%s' % ad_id
    match_key = 'terms:matched:%s' % target_id
    type = conn.hget('type', ad_id)
    if type == 'cpa':
        pipe.expire(match_key, 900)
        if action:
            click_key = 'actions:%s' % ad_id
    if action and type == 'cpa':
        pipe.incr('type:%s:actions:' % type)
    else:
        pipe.incr('type:%s:clicks:' % type)
    matched = list(conn.smembers(match_key))
    matched.append('')
    for word in matched:
        pipe.zincrby(click_key, float(1.0), word)
    result = pipe.execute()
    update_cpms(conn, ad_id)


def update_cpms(mconn, ad_id):
    pipe = mconn.pipeline(True)
    pipe.hget('type', ad_id)
    pipe.zscore('ad:base_value', ad_id)
    pipe.smembers('terms:' + ad_id)
    type, base_value, words = pipe.execute()
    which = 'clicks'
    if type == 'cpa':
        which = 'actions'
    pipe.get('type:%s:views:' % type)
    pipe.get('type:%s:%s' % (type, which))
    type_views, type_clicks = pipe.execute()
    AVERAGE_PER_1K[type] = (1000 * int(type_clicks or '1') / int(type_views or '1'))
    if type == 'cpm':
        return

    view_key = 'views:%s' % ad_id
    click_key = '%s:%s' % (which, ad_id)
    to_ecpm = TO_ECPM[type]
    pipe.zscore(view_key, '')
    pipe.zscore(click_key, '')
    ad_views, ad_licks = pipe.execute()
    if (ad_licks or 0) < 1:
        ad_ecpm = mconn.zscore('idx:ad:value', ad_id)
    else:
        ad_ecpm = to_ecpm(ad_views or 1, ad_licks or 0,
                          base_value)
        pipe.zadd('idx:ad:value', {ad_id: ad_ecpm})
    for word in words:
        pipe.zscore(view_key, word)
        pipe.zscore(click_key, word)
        result = pipe.execute()
        views, clicks = result[-2:]
        if (clicks or 0) < 1:
            continue
        word_ecpm = to_ecpm(views or 1, clicks or 0, base_value)
        bonus = word_ecpm - ad_ecpm
        pipe.zadd('idx:' + word, ad_id, bonus)
    pipe.execute()


def print_hi(name):
    print(f'Hi, {name}')  # 按 Ctrl+F8 切换断点。

    # 按间距中的绿色按钮以运行脚本。


if __name__ == '__main__':
    index_ad(
        mconn,
        '123',
        ['shenzhen', 'wuhan', 'changsha'],
        'The InfluxDB UI provides a web-based visual interface for interacting with and managing InfluxDB. The UI is packaged with InfluxDB and runs as part of the InfluxDB service. To access the UI, with InfluxDB running, visit localhost:8086 in your browser.',
        'cpm',
        0.45)
    index_ad(
        mconn,
        '456',
        ['shenzhen', 'changsha'],
        'Thank you for being part of our community! We welcome and encourage your feedback and bug reports for InfluxDB and this documentation. To find support, use the following resources',
        'cpa',
        3)
    index_ad(
        mconn,
        '789',
        ['shenzhen'],
        'Use the influx CLI or the InfluxDB API to get the runtime server configuration of your InfluxDB instance.Server configuration commands require an Operator token',
        'cpc',
        0.25)
    print(target_ads(mconn, ['wuhan', 'changsha'], 'InfluxDB UI provides a web-based visual interface'))
    record_targeting_result(mconn, '1000', '123', {'service', 'you'})
    record_click(mconn, '1000', '123')
    print_hi('PyCharm')

# 访问 https://www.jetbrains.com/help/pycharm/ 获取 PyCharm 帮助
